'
'   This module contains a user defined spreadsheet function called
'   sunevent(year, month, day, glong, glat, tz,event) that returns the
'   time of day of sunrise, sunset, or the beginning and end one of
'   three kinds of twilight
'
'   The method used here is adapted from Montenbruck and Pfleger's
'   Astronomy on the Personal Computer, 3rd Ed, section 3.8
'
'   the arguments for the function are as follows...
'   year, month, day - your date in zone time
'   glong - your longitude in degrees, west negative
'   glat - your latitude in degrees, north positive
'   tz - your time zone in decimal hours, west or 'behind' Greenwich negative
'   event - a code integer representing the event you want as follows
'       positive integers mean rising events
'       negative integers mean setting events
'       1 = sunrise                 -1  = sunset
'       2 = begin civil twilight    -2  = end civil twilight
'       3 = begin nautical twi      -3  = end nautical twi
'       4 = begin astro twi         -4  = end astro twi
'
'   the results are returned as a variant with either a time of day
'   in zone time or a string reporting an 'event not occuring' condition
'   event not occuring can be one of the following
'       .....    always below horizon, so no rise or set
'       *****    always above horizon, so no rise or set
'       -----    the particular rise or set event does not occur on that day
'
'   The function will produce meaningful results at all latitudes
'   but there will be a small range of latitudes around 67.43 degrees North or South
'   when the function might indicate a sunrise very close to noon (or a sunset
'   very soon after noon) where in fact the Sun is below the horizon all day
'   this behaviour relates to the approximate Sun position formulas in use
'
'   As always, the sunrise / set times relate to an earth which is smooth
'   and has no large obstructions on the horizon - you might get a close
'   approximation to this at sea but rarely on land. Accuracy more than 1 min
'   of time is not worth striving for - atmospheric refraction alone can
'   alter observed rise times by minutes
'
'   The module also defines a series of 'named funtions' based on sunevent()
'   as follows
'   astrotwilightstarts(date, tz, glong, glat)
'   nauticaltwilightstarts(date, tz, glong, glat)
'   civiltwilightstarts(date, tz, glong, glat)
'   sunrise(date, tz, glong, glat)
'   sunset(date, tz, glong, glat)
'   civiltwilightends(date, tz, glong, glat)
'   nauticaltwilightends(date, tz, glong, glat)
'   astrotwilightends(date, tz, glong, glat)
'
'   these functions  take a date in Excel date format and return times in
'   Excel time format (ie a day fraction) if there is an event on the day
'   If there isn't an event on the day, then you get #VALUE!
'   These functions lend themselves to plotting sunrise and set times on
'   charts - just multiply output in 'number' format by 24 to get the decimal
'   hours for the event
'
Private Function mjd(year As Integer, month As Integer, day As Integer) As Double
'
'   takes the year, month and day as a Gregorian calendar date
'   and returns the modified julian day number
'
    Dim a As Double
    Dim b As Double
    
    If (month <= 2) Then
        month = month + 12
        year = year - 1
        End If
    b = Fix(year / 400) - Fix(year / 100) + Fix(year / 4)
    a = 365# * year - 679004#
    mjd = a + b + Fix(30.6001 * (month + 1)) + day
End Function


Private Function frac(x As Double) As Double
'
'  returns the fractional part of x as used in minimoon and minisun
'
    Dim a As Double
    a = x - Fix(x)
    frac = a
End Function

Private Function range(x As Double) As Double
'
'   returns an angle in degrees in the range 0 to 360
'   used to condition the arguments for the Sun's orbit
'   in function minisun below
'
    Dim a As Double
    Dim b As Double
    b = x / 360
    a = 360 * (b - Fix(b))
    If (a < 0) Then
        a = a + 360
        End If
    range = a
End Function

Private Function hrsmin(t As Double) As String
'
'   takes a time as a decimal number of hours between 0 and 23.9999...
'   and returns a string with the time in hhmm format
'
    Dim hour As Double
    Dim min As Double
    hour = Fix(t)
    min = Fix((t - hour) * 60 + 0.5)
    hrsmin = Format(hour, "00") + Format(min, "00")
End Function


Private Function lmst(mjd As Double, glong As Double) As Double
'
'  Takes the mjd and the longitude (west negative) and then returns
'  the local sidereal time in hours. Im using Meeus formula 11.4
'  instead of messing about with UTo and so on
'
    Dim lst As Double
    Dim t As Double
    Dim d As Double
    d = mjd - 51544.5
    t = d / 36525#
    lst = range(280.46061837 + 360.98564736629 * d + 0.000387933 * t * t - t * t * t / 38710000)
    lmst = lst / 15# + glong / 15
End Function
    
Private Sub minisun(t As Double, RA As Double, dec As Double)
'
'   takes t (julian centuries since J2000.0) and empty variables ra and dec
'   sets ra and dec to the value of the Sun coordinates at t
'
'   positions claimed to be within 1 arc min by Montenbruck and Pfleger
'   improved version by thomas kraus with a better accuracy



   Dim longitude_soleil As Double
Dim longitude_vraie_soleil As Double
Dim longitude_apparente_soleil As Double
Dim anomalie_soleil As Double
Dim anomalie_vraie_soleil As Double
Dim excentricite_orbite_terre As Double
Dim centre_soleil As Double
Dim obliquite_ecliptique As Double
Dim declinaison_soleil As Double
Dim ascension_soleil As Double
     
    
     sjulien = t
     
     
    
     
   jd1 = t * 36525 + 2451545
   
   es = (jd1 - 2415020) / 36525



a1 = 153.23 + 22518.7541 * es
a2 = 216.57 + 45037.5082 * es
a3 = 312.69 + 32964.3577 * es
a4 = 350.74 + 445267.1142 * es - 0.00144 * es * es
a5 = 231.19 + 20.2 * es

b1 = 0.00134 * Cos(a1 * 3.14159265358979 / 180)
b2 = 0.00154 * Cos(a2 * 3.14159265358979 / 180)
b3 = 0.002 * Cos(a3 * 3.14159265358979 / 180)
b4 = 0.00179 * Sin(a4 * 3.14159265358979 / 180)
b5 = 0.00178 * Sin(a5 * 3.14159265358979 / 180)

bges = b1 + b2 + b3 + b4 + b5
     
     
     

    longitude_soleil = 280.46645 + (36000.76983 * sjulien) + (0.0003032 * sjulien ^ 2)
    longitude_soleil = range(longitude_soleil)

    anomalie_soleil = 357.5291 + (35999.0503 * sjulien) - (0.0001559 * sjulien ^ 2) - (0.00000048 * sjulien ^ 3)
    anomalie_soleil = range(anomalie_soleil)

    excentricite_orbite_terre = 0.01670817 - (0.000042037 * sjulien) - (0.0000001236 * sjulien ^ 2)
                       
    centre_soleil = ((1.9146 - (0.004817 * sjulien) - _
                    (0.000014 * sjulien ^ 2)) * Sin((3.1415926535 / 180) * (anomalie_soleil))) + _
                    ((0.019993 - (0.000101 * sjulien)) * Sin((3.1415926535 / 180) * (2 * anomalie_soleil))) + _
                    (0.00029 * Sin((3.1415926535 / 180) * (3 * anomalie_soleil)))
    
    centre_soleil = range(centre_soleil)

        anomalie_vraie_soleil = anomalie_soleil + centre_soleil
    
    anomalie_vraie_soleil = range(anomalie_vraie_soleil)

        longitude_vraie_soleil = longitude_soleil + centre_soleil
    
    longitude_vraie_soleil = range(longitude_vraie_soleil)

        longitude_apparente_soleil = longitude_vraie_soleil - 0.00569 - _
                    0.00478 * Sin((3.1415926535 / 180) * (125.04 - (1934.136 * sjulien))) + bges
    
    longitude_apparente_soleil = range(longitude_apparente_soleil)

        obliquite_ecliptique = 23.43929111 - (0.01300417 * sjulien) - (0.000001639 * sjulien ^ 2) + _
                    (0.0000005036 * sjulien ^ 3)
    
    obliquite_ecliptique = range(obliquite_ecliptique)

        ascension_soleilx = Cos((3.1415926535 / 180) * obliquite_ecliptique) * Sin((3.1415926535 / 180) * longitude_vraie_soleil)
                      
        ascension_soleilY = Cos((3.1415926535 / 180) * longitude_vraie_soleil)
                  
                                  
                          
     
  
  decx = Sin((3.1415926535 / 180) * obliquite_ecliptique) * Sin((3.1415926535 / 180) * (longitude_vraie_soleil))
  
  dec = Atn(decx / Sqr(1 - decx * decx)) * 180 / 3.14159265358979


    
ascension_soleil = Atn(ascension_soleilx / ascension_soleilY) * 180 / 3.14159265358979

If ascension_soleilY < 0 Then
   ascension_soleil = 180 + ascension_soleil
End If
     
RA = range(ascension_soleil) / 15

RA = RA
dec = dec

End Sub


Private Sub quad(ym As Double, yz As Double, yp As Double, nz As Integer, z1 As Double, z2 As Double, xe As Double, ye As Double)
'
'  finds the parabola throuh the three points (-1,ym), (0,yz), (1, yp)
'  and sets the coordinates of the max/min (if any) xe, ye
'  the values of x where the parabola crosses zero (z1, z2)
'  and the nz number of roots (0, 1 or 2) within the interval [-1, 1]
'
    Dim a As Double
    Dim b As Double
    Dim c As Double
    Dim dis As Double
    Dim dx As Double

    nz = 0
    a = 0.5 * (ym + yp) - yz
    b = 0.5 * (yp - ym)
    c = yz
    xe = -b / (2 * a)
    ye = (a * xe + b) * xe + c
    dis = b * b - 4# * a * c
    If (dis > 0) Then
        dx = 0.5 * Sqr(dis) / Abs(a)
        z1 = xe - dx
        z2 = xe + dx
        If (Abs(z1) <= 1#) Then nz = nz + 1
        If (Abs(z2) <= 1#) Then nz = nz + 1
        If (z1 < -1#) Then z1 = z2
        End If
End Sub

Private Function SinAltSun(mjd0 As Double, hour As Double, glong As Double, cglat As Double, sglat As Double) As Double
'
'  this rather mickey mouse function takes a lot of
'  arguments and then returns the sine of the altitude of
'  the object labelled by iobj. iobj = 1 is moon, iobj = 2 is sun
'
    Dim mjd As Double
    Dim t As Double
    Dim RA As Double
    Dim dec As Double
    Dim tau As Double
    Dim salt As Double
    Dim rads As Double
    rads = 0.0174532925
    mjd = mjd0 + hour / 24#
    t = (mjd - 51544.5) / 36525#
    Call minisun(t, RA, dec)
    ' hour angle of object
    tau = 15# * (lmst(mjd, glong) - RA)
    ' sin(alt) of object using the conversion formulas
    salt = sglat * sin(rads * dec) + cglat * Cos(rads * dec) * Cos(rads * tau)
    SinAltSun = salt
End Function

'
'   Worksheet functions below....
'
    
Function sunevent(year As Integer, month As Integer, day As Integer, tz As Double, glong As Double, glat As Double, EventType As Integer) As String
'
'   This is the function that does most of the work
'
    Dim sglong As Double, sglat As Double, cglat As Double, ddate As Double, ym As Double
    Dim yz As Double, above As Integer, utrise As Double, utset As Double
    Dim yp As Double, nz As Integer, rise As Integer, sett As Integer, j As Integer
    Dim hour As Double, z1 As Double, z2 As Double, rads As Double, xe As Double, ye As Double
    Dim AlwaysUp As String, AlwaysDown As String, OutString As String, NoEvent As String
    Dim sinho(5) As Double
    rads = 0.0174532925
    AlwaysUp = "****"
    AlwaysDown = "...."
    NoEvent = "----"
    
'
'   Set up the array with the 4 values of sinho needed for the 4
'   kinds of sun event
'
    sinho(1) = sin(rads * -0.833)     'sunset upper limb simple refraction
    sinho(2) = sin(rads * -6#)        'civil twi
    sinho(3) = sin(rads * -12#)       'nautical twi
    sinho(4) = sin(rads * -18#)       'astro twi
    sglat = sin(rads * glat)
    cglat = Cos(rads * glat)
    ddate = mjd(year, month, day) - tz / 24
'
'   main loop takes each value of sinho in turn and finds the rise/set
'   events associated with that altitude of the Sun
'
    j = Abs(EventType)
        nz = 0
        z1 = 0
        z2 = 0
        xe = 0
        ye = 0
        rise = 0
        sett = 0
        above = 0
        hour = 1#
        ym = SinAltSun(ddate, hour - 1#, glong, cglat, sglat) - sinho(j)
        If (ym > 0#) Then above = 1
        '
        '  the while loop finds the sin(alt) for sets of three consecutive
        '  hours, and then tests for a single zero crossing in the interval
        '  or for two zero crossings in an interval or for a grazing event
        '  The flags rise and sett are set accordingly
        '
        Do While (hour < 25 And (sett = 0 Or rise = 0))
            yz = SinAltSun(ddate, hour, glong, cglat, sglat) - sinho(j)
            yp = SinAltSun(ddate, hour + 1#, glong, cglat, sglat) - sinho(j)
            Call quad(ym, yz, yp, nz, z1, z2, xe, ye)
            ' case when one event is found in the interval
            If (nz = 1) Then
                If (ym < 0#) Then
                    utrise = hour + z1
                    rise = 1
                Else
                    utset = hour + z1
                    sett = 1
                End If
            End If ' end of nz = 1 case
            '
            '   case where two events are found in this interval
            '   (rare but whole reason we are not using simple iteration)
            '
            If (nz = 2) Then
                If (ye < 0#) Then
                    utrise = hour + z2
                    utset = hour + z1
                Else
                    utrise = hour + z1
                    utset = hour + z2
                End If
            rise = 1
            sett = 1
            End If
            '
            '   set up the next search interval
            '
            ym = yp
            hour = hour + 2#

        Loop ' end of while loop
            '
            ' now search has completed, we compile the string to pass back
            ' to the user. The string depends on several combinations
            ' of the above flag (always above or always below) and the rise
            ' and sett flags
            '
        If (rise = 1 Or sett = 1) Then
            If (rise = 1) Then
                If (EventType > 0) Then OutString = hrsmin(utrise)
            Else
                If (EventType > 0) Then OutString = NoEvent
            End If
            If (sett = 1) Then
                If (EventType < 0) Then OutString = hrsmin(utset)
            Else
                If (EventType < 0) Then OutString = NoEvent
            End If
        Else
            If (above = 1) Then
                OutString = AlwaysUp
            Else
                OutString = AlwaysDown
            End If
        End If
        sunevent = OutString
End Function

Function sunrise(ddate As Date, tz As Double, glong As Double, glat As Double) As Double
'
'   simple way of calling sunevent() using the Excel date format
'   returns just the sunrise time or NULL if no event
'   I used the day(), month() and year() functions in excel to allow
'   portability to the MAC (different date serial numbers)
'
    Dim EventTime As Double, hour As Double, minfrac As Double
    Dim out As String
    out = sunevent(year(ddate), month(ddate), day(ddate), tz, glong, glat, 1)
    If (out = "...." Or out = "----" Or out = "****") Then
        EventTime = Null
    Else
        hour = Fix(Val(out) / 100)
        minfrac = (Val(out) - 100 * hour) / 60
        hour = hour + minfrac
        hour = hour / 24
        EventTime = hour
    End If
    sunrise = EventTime
End Function

Function sunset(ddate As Date, tz As Double, glong As Double, glat As Double) As Double
'
'   simple way of calling sunevent() using the Excel date format
'   returns just the sunset time or ****, ...., ---- as approptiate in a string
'   I used the day(), month() and year() functions in excel to allow
'   portability to the MAC (different date serial number base)
'
    Dim EventTime As Double, hour As Double, minfrac As Double
    Dim out As String
    out = sunevent(year(ddate), month(ddate), day(ddate), tz, glong, glat, -1)
    If (out = "...." Or out = "----" Or out = "****") Then
        EventTime = Null
    Else
        hour = Fix(Val(out) / 100)
        minfrac = (Val(out) - 100 * hour) / 60
        hour = hour + minfrac
        hour = hour / 24
        EventTime = hour
    End If
    sunset = EventTime
End Function

Function CivilTwilightStarts(ddate As Date, tz As Double, glong As Double, glat As Double) As Double
'
'   simple way of calling sunevent() using the Excel date format
'   returns just the start of civil twilight time or ****, ...., ---- as approptiate
'   I used the day(), month() and year() functions in excel to allow
'   portability to the MAC (different date serial numbers)
'
    Dim EventTime As Double, hour As Double, minfrac As Double
    Dim out As String
    out = sunevent(year(ddate), month(ddate), day(ddate), tz, glong, glat, 2)
    If (out = "...." Or out = "----" Or out = "****") Then
        EventTime = Null
    Else
        hour = Fix(Val(out) / 100)
        minfrac = (Val(out) - 100 * hour) / 60
        hour = hour + minfrac
        hour = hour / 24
        EventTime = hour
    End If
    CivilTwilightStarts = EventTime
End Function

Function CivilTwilightEnds(ddate As Date, tz As Double, glong As Double, glat As Double) As Double
'
    Dim EventTime As Double, hour As Double, minfrac As Double
    Dim out As String
    out = sunevent(year(ddate), month(ddate), day(ddate), tz, glong, glat, -2)
    If (out = "...." Or out = "----" Or out = "****") Then
        EventTime = Null
    Else
        hour = Fix(Val(out) / 100)
        minfrac = (Val(out) - 100 * hour) / 60
        hour = hour + minfrac
        hour = hour / 24
        EventTime = hour
    End If
    CivilTwilightEnds = EventTime
    End Function

Function NauticalTwilightStarts(ddate As Date, tz As Double, glong As Double, glat As Double) As Double
'
    Dim EventTime As Double, hour As Double, minfrac As Double
    Dim out As String
    out = sunevent(year(ddate), month(ddate), day(ddate), tz, glong, glat, 3)
    If (out = "...." Or out = "----" Or out = "****") Then
        EventTime = Null
    Else
        hour = Fix(Val(out) / 100)
        minfrac = (Val(out) - 100 * hour) / 60
        hour = hour + minfrac
        hour = hour / 24
        EventTime = hour
    End If
    NauticalTwilightStarts = EventTime
End Function

Function NauticalTwilightEnds(ddate As Date, tz As Double, glong As Double, glat As Double) As Double
'
    Dim EventTime As Double, hour As Double, minfrac As Double
    Dim out As String
    out = sunevent(year(ddate), month(ddate), day(ddate), tz, glong, glat, -3)
    If (out = "...." Or out = "----" Or out = "****") Then
        EventTime = Null
    Else
        hour = Fix(Val(out) / 100)
        minfrac = (Val(out) - 100 * hour) / 60
        hour = hour + minfrac
        hour = hour / 24
        EventTime = hour
    End If
    NauticalTwilightEnds = EventTime
End Function

Function AstroTwilightStarts(ddate As Date, tz As Double, glong As Double, glat As Double) As Double
'
    Dim EventTime As Double, hour As Double, minfrac As Double
    Dim out As String
    out = sunevent(year(ddate), month(ddate), day(ddate), tz, glong, glat, 4)
    If (out = "...." Or out = "----" Or out = "****") Then
        EventTime = Null
    Else
        hour = Fix(Val(out) / 100)
        minfrac = (Val(out) - 100 * hour) / 60
        hour = hour + minfrac
        hour = hour / 24
        EventTime = hour
    End If
    AstroTwilightStarts = EventTime
End Function

Function AstroTwilightEnds(ddate As Date, tz As Double, glong As Double, glat As Double) As Double
'
    Dim EventTime As Double, hour As Double, minfrac As Double
    Dim out As String
    out = sunevent(year(ddate), month(ddate), day(ddate), tz, glong, glat, -4)
    If (out = "...." Or out = "----" Or out = "****") Then
        EventTime = Null
    Else
        hour = Fix(Val(out) / 100)
        minfrac = (Val(out) - 100 * hour) / 60
        hour = hour + minfrac
        hour = hour / 24
        EventTime = hour
    End If
    AstroTwilightEnds = EventTime
End Function








